﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BitTorrent.Bencode.Exceptions;
using System.Diagnostics.Contracts;

namespace BitTorrent.Bencode {

    internal sealed class BencodeFactory {
        public static byte[] Encode(Object obj) {
            Contract.Requires(obj != null);

            return obj.Encode();
        }

        public static Object Decode(byte[] data) {
            Contract.Requires(data != null);
            
            using (var memory = new MemoryStream(data)) {
                return Decode(memory);
            }
        }

        public static T Decode<T>(byte[] data) where T : Object {
            Contract.Requires(data != null);

            return (T)Decode(data);
        }

        public static Object Decode(Stream stream) {
            Contract.Requires(stream != null);

            using (var reader = new BinaryReader(stream, Encoding.UTF8)) {
                return Decode(reader);
            }
        }

        public static T Decode<T>(Stream stream) where T : Object {
            Contract.Requires(stream != null);

            return (T)Decode(stream);
        }

        public static Object Decode(BinaryReader reader) {
            if (reader.BaseStream.Length == 0) {
                return null;
            }

            Object result;
            // Peek because the first char could be a number
            char start = (char)reader.PeekChar();

            switch (start) {
                case 'i':
                    reader.ReadChar();
                    result = DecodeInteger(reader);
                    break;
                case 'l':
                    reader.ReadChar();
                    result = DecodeList(reader);
                    break;
                case 'd':
                    reader.ReadChar();
                    result = DecodeDictonary(reader);
                    break;
                default:
                    int integer;
                    if (int.TryParse(new string(start, 1), out integer)) {
                        result = DecodeByteString(reader);
                    }
                    else {
                        throw new BencodeException(ErrorResource.MallformedStringMessage);
                    }

                    break;
            }

            return result;
        }

        public static T Decode<T>(BinaryReader stream) where T : Object {
            Contract.Requires(stream != null);

            return (T)Decode(stream);
        }

        private static Collections.Dictionary DecodeDictonary(BinaryReader reader) {
            Contract.Ensures(Contract.Result<Collections.Dictionary>() != null);
            var dictonary = new Collections.Dictionary();

            while (reader.PeekChar() != 'e') {
                var key = Decode(reader);

                if (!(key is ByteString)) {
                    throw new BencodeException(ErrorResource.DictionaryKeyTypeMismatchMessage);
                }

                var value = Decode(reader);

                dictonary.Add((ByteString)key, value);
            }

            reader.ReadChar();

            return dictonary;
        }

        private static Collections.List DecodeList(BinaryReader reader) {
            Contract.Ensures(Contract.Result<Collections.List>() != null);
            var list = new Collections.List();

            while (reader.PeekChar() != 'e') {
                var item = Decode(reader);
                list.Add(item);
            }

            reader.ReadChar();

            return list;
        }

        private static Integer DecodeInteger(BinaryReader reader) {
            return new Integer(ReadUntil(reader, (byte)'e'));
        }

        private static ByteString DecodeByteString(BinaryReader reader) {
            Contract.Ensures(Contract.Result<ByteString>() != null);

            int length = int.Parse(Encoding.UTF8.GetString(ReadUntil(reader, (byte)':')));

            if (reader.BaseStream.Length < length) {
                throw new BencodeException(ErrorResource.ByteStringSizeMessage);
            }

            byte[] data = new byte[length];
            reader.Read(data, 0, length);

            return new ByteString(data);
        }

        private static byte[] ReadUntil(BinaryReader reader, byte stop) {
            long pos = reader.BaseStream.Position;
            int length = 0;

            try {
                while (reader.ReadByte() != stop) {
                    ++length;
                }
            } catch (EndOfStreamException ex) {
                throw new BencodeException(BitTorrent.ErrorResource.EndOfBencodeStreamMessage, ex);
            }
            
            reader.BaseStream.Position = pos;

            byte[] buffer = new byte[length];
            reader.Read(buffer, 0, length);
            reader.ReadByte(); // we are not interested in the stop byte...

            return buffer;
        }
    }
}
